import {
  ClassDeclaration,
  CommentRange,
  JSDoc,
  MethodDeclaration,
  ParameterDeclaration,
  Project,
  Symbol,
  SyntaxKind,
  Type
} from 'ts-morph';
import {isEmpty, isNumber} from 'lodash';
import * as fs from 'fs';


/**
 * 参数对象类型
 */
type param = {
  type: string;
  name: string;
  desc: string;
  attr?: {
    //是否必须
    require?: boolean;
    //是否忽略
    ignored?: boolean;
    //是否为对象
    isObject?: boolean;
  };
  children?: param[];
};

enum Media {
  JSON = 'application/json',
  FORM_DATA = 'multipart/form-data',
  OCTET_STREAM = 'application/octet-stream',
}

/**
 * 控制器类型
 */
type controller = {
  url: string;
  clss: ClassDeclaration;
  command: string;
  flag: string;
  methods?: {
    url: string;
    method: MethodDeclaration;
    urlType: 'post' | 'get' | 'put' | 'delete';
    //类型 [application/json , multipart/form-data ]
    requestType: Media,
    //类型 [application/json, application/octet-stream]
    responseType: Media,
    command: string;
    params: param[];
    return?: param[];
    flag: string;
  }[];
};

/**
 * 配置信息
 */
type config = {
  //属性上标记@ignored 生成时忽略
  //@Rule().required() 生成时标记必填
  //扫描路径
  scanPath?: string;
  //输入路径，默认当前项目 ./apidoc/openapi.json
  output?: string;
  //需要生成的controller类名列表, 默认所有
  controllers?: string[];
  //需要生成的methods方法名列表，默认所有
  methods?: string[];
  //需要排除的公共字段
  excludeFields?: string[];
};

let func = (config: config) => {
    const project = new Project();
    project.addSourceFilesAtPaths(config?.scanPath ?? 'src/controller/**/*.ts');
    const sourceFiles = project.getSourceFiles();
    const controllers: controller[] = [];

    /**
     * 获取注释
     * @param comment
     */
    let joinComment = (comment: CommentRange[] | JSDoc[]) => {
      if (isEmpty(comment)) return null;
      return comment[0]?.getText().replace(new RegExp('\\*', 'g'), '').replace(new RegExp('/', 'g'), '').replace(new RegExp('\n', 'g'), '').trim()?.split('@')[0]?.trim();
    };

    /**
     *  获取类型
     * @param typeCls 类型
     * @param isBase 是否为基础类型
     */
    let getType = (typeCls: Type | undefined, isBase: boolean = false) => {
      if (!typeCls) {
        return 'string';
      }
      let type: string;
      if (typeCls.isTuple()) {
        type = 'array';
      } else if (typeCls.isArray()) {
        type = 'array';
      } else if (typeCls.isEnum()) {
        type = 'string';
      } else if (typeCls.isObject()) {
        type = 'object';
      } else {
        if (!isEmpty(typeCls.getUnionTypes()) && !isBase) {
          type = typeCls.getUnionTypes()[0].getText();
        } else {
          type = typeCls.getText();
        }
        if (type && type === 'true' || type === 'false') {
          type = 'boolean';
        } else {
          type = 'string';
        }
      }
      return type;
    };

    /**
     * 递归获取子类型
     * @param type 遍历类型
     * @param exists 已存在的对象集合
     * @param property 当前属性
     * @param obj 当前主类
     */
    let subClasType = (type: Type, exists: Set<string>, property: Symbol, obj: Type) => {
      function getChildNodes(typeCls: Type, children: param[]) {
        if (typeCls && typeCls.isTuple()) {
          children.push(...subClasType(typeCls, exists, property, obj)!);
        } else if (typeCls && typeCls.isArray()) {
          children.push(...subClasType(typeCls.getArrayElementType()!, exists, property, obj)!);
        } else if (typeCls && typeCls.isObject()) {
          children.push(...(subClasType(typeCls, exists, property, obj) ?? []));
        } else if (typeCls && obj && typeCls.isTypeParameter()) {
          const typeArr = obj.getTargetType().getTypeArguments();
          let actType: Type;
          typeArr.forEach((v, index) => {
            if (v.getText() === typeCls.getText()) {
              actType = obj.getTypeArguments()[index];
            }
          });
          if (actType) {
            children.push(...(subClasType(actType, exists, property, obj) ?? []));
          }

        } else {
          children.push({
            type: getType(typeCls, true),
            name: '',
            desc: '',
            attr: {
              require: false,
              ignored: false,
              isObject: false,
            },
            children: null,
          });
        }
        return children;
      }

      if (type.isTuple()) {
        const typeClsArr = type.getTupleElements();
        let children: param[] = [];
        for (const typeCls of typeClsArr) {
          getChildNodes(typeCls, children);
        }
        children.forEach((v, i) => {
          v.name = `$${i}`;
        });
        return children;
      } else if (type.isArray()) {
        //取第一个对象，进行递归
        const typeCls = type.getArrayElementType();
        let children: param[] = [];
        getChildNodes(typeCls!, children);

        return children;
      } else if (type.isObject()) {
        if (exists.has(type.getText() + property.getName())) {
          return [] as param[];
        } else {
          exists.add(type.getText() + property.getName());
        }
        return type.getProperties().map(_ => {
          let docs = joinComment(_.getDeclarations()[0]?.getTrailingCommentRanges()) ?? joinComment(_.getDeclarations()[0]?.getLeadingCommentRanges()) ?? joinComment(_.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
          const typeCls = _.getValueDeclaration()?.getType() ?? _.getDeclaredType() ?? _?.getValueDeclaration()?.getType();
          let type = getType(typeCls);
          //处理枚举值
          if (typeCls.isEnum()) {
            try {
              const enums = _.getValueDeclaration()?.getType().getSymbol()?.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.EnumMember) ?? [];

              let dval = '';
              for (const enumVal of enums) {
                let name = joinComment(enumVal?.getTrailingCommentRanges()) ?? joinComment(enumVal?.getLeadingCommentRanges()) ?? joinComment(enumVal?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
                dval += ` ${enumVal.getValue()}:${name} `;
                if (isNumber(enumVal.getValue())) {
                  type = 'integer';
                } else {
                  type = 'string';
                }
              }
              const enumsDesc = `[枚举值: ${dval}]`;
              docs += enumsDesc;
            } catch (e) {
            }
          }

          let children: param[] = [];
          if (typeCls.isArray()) {
            children = subClasType(typeCls.getArrayElementType()!, exists, _, obj)!;
          } else if (typeCls.isObject()) {
            children = subClasType(typeCls, exists, _, obj)!;
          }

          const require = _.getDeclarations()
            .map(_ => _.getText())
            .join('')
            .includes('required()');
          const ignored = (
            _?.getValueDeclaration()
              ?.getLeadingCommentRanges()
              .map(_ => _.getText())
              .join('') ??
            '' +
            _?.getValueDeclaration()
              ?.getTrailingCommentRanges()
              .map(_ => _.getText())
              .join('') ??
            ''
          ).includes('@ignored');

          const name = _.getName();
          return {
            desc: docs,
            type: type,
            name: name,
            attr: {
              require: require,
              ignored: ignored,
              isObject: true,
            },
            children: children,
          } as param;
        });
      } else {
        //处理数组里面是基本类型
        return [
          {
            type: type.getText() ?? 'string',
            name: '',
            desc: '',
            attr: {
              require: false,
              ignored: false,
              isObject: false,
            },
            children: null,
          },
        ];
      }
    };

    /**
     * 解析方法参数
     * @param parameters
     * @param method
     */
    let handlerParams = (parameters: ParameterDeclaration[], method: MethodDeclaration) => {
      const params: param[] = [];


      parameters.forEach(param => {
        let type = param.getType();
        const isBody = param.getDecorators().some(_ => _.getName().toLowerCase() === 'body');
        const isFields = param.getDecorators().some(_ => _.getName().toLowerCase() === 'fields');
        const isFile = param.getDecorators().some(_ => _.getName().toLowerCase() === 'files');
        if (!(isBody || isFields || isFile)) {
          return;
        }
        //处理文件上传
        if (isFile) {
          params.push({
            desc: '文件',
            type: 'binary',
            name: 'file',
            attr: {
              require: true,
              ignored: false,
              isObject: false,
            },
            children: null,
          });
          return;
        }

        /**
         * 处理类型细节
         * @param type
         * @param param
         */
        function processType(type: Type, param: ParameterDeclaration) {
          if (type.isArray() || type.isTuple()) {
            type = type.getArrayElementType() ?? type.getTupleElements()[0];
          }
          if (type.isObject() || type.isEnum()) {
            type
              .getProperties()
              .forEach(property => {
                let docs =
                  joinComment(property.getDeclarations()[0]?.getTrailingCommentRanges()) ??
                  joinComment(property.getDeclarations()[0]?.getLeadingCommentRanges()) ??
                  joinComment(property.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.JSDoc)) ??
                  '';
                const require = property
                  .getDeclarations()
                  .map(_ => _.getText())
                  .join('')
                  .includes('required()');
                const ignored = (
                  property
                    .getValueDeclaration()
                    ?.getLeadingCommentRanges()
                    .map(_ => _.getText())
                    .join('') ??
                  '' +
                  property
                    .getValueDeclaration()
                    ?.getTrailingCommentRanges()
                    .map(_ => _.getText())
                    .join('') ??
                  ''
                ).includes('@ignored');
                const typeCls = property.getValueDeclaration()?.getType();
                let typeStr = getType(typeCls);
                let children: param[] = [];
                let exists = new Set<string>();
                if (typeCls && typeCls.isArray()) {
                  children = subClasType(typeCls, exists, property, type);
                } else if (typeCls && typeCls.isObject()) {
                  children = subClasType(typeCls, exists, property, type);
                } else if (typeCls && typeCls.isEnum()) {
                  try {
                    const enums = property.getValueDeclaration()?.getType().getSymbol()?.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.EnumMember) ?? [];

                    let dval = '';
                    for (const enumVal of enums) {
                      let name = joinComment(enumVal?.getTrailingCommentRanges()) ?? joinComment(enumVal?.getLeadingCommentRanges()) ?? joinComment(enumVal?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
                      dval += ` ${enumVal.getValue()}:${name} `;
                      if (isNumber(enumVal.getValue())) {
                        typeStr = 'integer';
                      } else {
                        typeStr = 'string';
                      }
                    }
                    const enumsDesc = `[枚举值: ${dval}]`;
                    docs += enumsDesc;
                  } catch (e) {
                  }
                }

                const name = property.getName();
                params.push({
                  desc: docs,
                  type: typeStr,
                  name: name,
                  attr: {
                    require: require,
                    ignored: ignored,
                    isObject: false,
                  },
                  children: children,
                });
              });
          } else {

            const name = param.getName();
            const paramDocs = method.getDescendantsOfKind(SyntaxKind.JSDocParameterTag);
            const paramDoc = paramDocs?.find(_ => _.getName() === name);

            !isFields && params.push({
              desc: paramDoc?.getCommentText() ?? '',
              type: type.getText(),
              name: name,
            });
          }
        }

        //处理其他类型
        processType(type, param);
      });
      return params;
    };

    /**
     * 处理返回封装对象中的结果
     * @param returnAct
     */
    let processFuncOrApiResult = (returnAct: Type) => {
      const foo = returnAct.getText().toString();
      if (foo.includes('<') && foo.includes('>')) {
        //直接解析，并解析泛型对象

        let returnAct1 = !isEmpty(returnAct.getTypeArguments()[0]) ? returnAct.getTypeArguments()[0] : returnAct.getAliasTypeArguments()[0];
        let typeAct = returnAct1;
        if (!returnAct1) {
          return [];
        }
        if (returnAct1.isArray()) {
          returnAct1 = returnAct1.getArrayElementType()!;
        }
        if (returnAct1.isObject()) {
          let children = returnAct1?.getProperties().map(property => {
            let desc =
              joinComment(property.getDeclarations()[0]?.getTrailingCommentRanges()) ??
              joinComment(property.getDeclarations()[0]?.getLeadingCommentRanges()) ??
              joinComment(property.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.JSDoc)) ??
              '';
            const require = property
              .getDeclarations()
              .map(_ => _.getText())
              .join('')
              .includes('required()');
            const ignored = (
              property
                .getValueDeclaration()
                ?.getLeadingCommentRanges()
                .map(_ => _.getText())
                .join('') ??
              '' +
              property
                .getValueDeclaration()
                ?.getTrailingCommentRanges()
                .map(_ => _.getText())
                .join('') ??
              ''
            ).includes('@ignored');
            let typeCls = property.getValueDeclaration()?.getType();
            let type = getType(typeCls);
            let children: param[] = [];
            let exists = new Set<string>();
            //泛型类型
            let subType: Type;

            if (typeCls.isTypeParameter()) {
              const typeArr = returnAct1.getTargetType().getTypeArguments();
              typeArr.forEach((v, index) => {
                if (v.getText() === typeCls.getText()) {
                  typeCls = returnAct1.getTypeArguments()[index];
                  type = getType(typeCls)
                }
              })
            }
            if (typeCls && typeCls.isArray()) {
              if (subType) {
                //泛型中获取
                typeCls = subType;
              }
              children = subClasType(typeCls, exists, property, returnAct1 ?? returnAct)!;
            } else if (typeCls && typeCls.isObject()) {
              children = subClasType(typeCls, exists, property, returnAct1 ?? returnAct);
            } else if (typeCls && typeCls.isEnum()) {
              try {
                const enums = property.getValueDeclaration()?.getType().getSymbol()?.getDeclarations()[0]?.getDescendantsOfKind(306) ?? [];

                let dval = '';
                for (const enumVal of enums) {
                  let name = joinComment(enumVal?.getTrailingCommentRanges()) ?? joinComment(enumVal?.getLeadingCommentRanges()) ?? joinComment(enumVal?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
                  dval += ` ${enumVal.getValue()}:${name} `;
                  if (isNumber(enumVal.getValue())) {
                    type = 'integer';
                  } else {
                    type = 'string';
                  }
                }
                const enumsDesc = `[枚举值: ${dval}]`;
                desc += enumsDesc;
              } catch (e) {
              }
            }

            const name = property.getName();

            return {
              desc: desc,
              name: name,
              type: type,
              attr: {
                require: require,
                ignored: ignored,
                isObject: true,
              },
              children: children,
            } as param;
          });
          //将result先解析，在存放内部数据
          return returnAct.getProperties().map(prop => {
            const docs =
              joinComment(prop.getDeclarations()[0]?.getTrailingCommentRanges()) ?? joinComment(prop.getDeclarations()[0]?.getLeadingCommentRanges()) ?? joinComment(prop.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';

            return {
              desc: docs,
              name: prop.getName(),
              type: prop.getValueDeclaration()?.getType().getText() === 'T' ? getType(typeAct) : getType(prop.getValueDeclaration()?.getType()),
              children: prop.getValueDeclaration()?.getType().getText() === 'T' ? children : null,
            } as param;
          });
        } else {
          return returnAct.getProperties().map(prop => {
            const docs =
              joinComment(prop.getDeclarations()[0]?.getTrailingCommentRanges()) ?? joinComment(prop.getDeclarations()[0]?.getLeadingCommentRanges()) ?? joinComment(prop.getDeclarations()[0]?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
            return {
              desc: docs,
              name: prop.getName(),
              type: prop.getValueDeclaration()?.getType().getText() === 'T' ? getType(returnAct1) : getType(prop.getValueDeclaration()?.getType()),
            } as param;
          });
        }
      } else {
        //暂不支持其他格式返回
        return [];
      }
    };

    let isRequestType = (params: ParameterDeclaration[]) => {
      const isBody = params.find(_ => _.getDecorators().some(_ => _.getName().toLowerCase() === 'body'));
      if (isBody) {
        return Media.JSON;
      }
      const isDataForm = params.find(_ => _.getDecorators().some(_ => _.getName().toLowerCase() === 'files'));
      if (isDataForm) {
        return Media.FORM_DATA;
      }
      return Media.JSON;
    }

    let isResponseType = (method: MethodDeclaration) => {
      const docs = method.getDescendantsOfKind(SyntaxKind.JSDoc);
      const isDataForm = docs.find(_ => _.getTags().some(_ => _.getTagName().toLowerCase() === 'file'));
      if (isDataForm) {
        return Media.OCTET_STREAM;
      }
      return Media.JSON;
    }

    /**
     * 处理返回值
     * @param returnType
     */
    let handlerReturns = (returnType: Type) => {
      if (!returnType) {
        return [];
      }
      let params: param[];
      if (returnType.getText().toLowerCase().includes('promise')) {
        const returnAct = returnType.getTypeArguments()[0];
        params = processFuncOrApiResult(returnAct);
      } else {
        params = processFuncOrApiResult(returnType);
      }

      return params;
    };


    sourceFiles.forEach(sourceFile =>
      sourceFile.getClasses().forEach(classDecl => {
          if (!isEmpty(config.controllers) && !config.controllers?.includes(classDecl.getName() ?? '')) {
            return;
          }
          let controllerInfo: controller;
          classDecl.getDecorators().forEach(decorator => {
            if (decorator.getName().toLowerCase() === 'controller') {
              controllerInfo = {
                url: decorator.getArguments()[0].getText().replace(new RegExp("'|\"", 'g'), ''),
                clss: classDecl,
                flag: classDecl.getName(),
                command: joinComment(classDecl.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '',
                methods: [],
              };
            }
          });
          const methods = classDecl.getMethods();
          methods.forEach(method => {
            if (!isEmpty(config.methods) && !config.methods?.includes(method.getName() ?? '')) {
              return;
            }
            const decorators = method.getDecorators();
            if (!isEmpty(decorators)) {
              decorators.forEach(decorator => {
                  let urlType = decorator.getName().toLowerCase();
                  if (urlType && !['post', 'get'].includes(urlType)) {
                    return;
                  }

                  const methodUrl = decorator.getArguments()[0]?.getText();
                  const methodDesc = joinComment(method?.getTrailingCommentRanges()) ?? joinComment(method?.getLeadingCommentRanges()) ?? joinComment(method?.getDescendantsOfKind(SyntaxKind.JSDoc)) ?? '';
                  let params = handlerParams(method.getParameters(), method);
                  let returns = handlerReturns(method.getReturnType());
                  controllerInfo.methods?.push({
                    url: methodUrl.replace(new RegExp("'|\"", 'g'), ''),
                    command: methodDesc,
                    urlType: urlType as any,
                    requestType: isRequestType(method.getParameters()),
                    responseType: isResponseType(method),
                    method: method,
                    params: params,
                    return: returns,
                    flag: method.getName(),
                  });
                }
              );
            }
          });
          if (controllerInfo!) {
            controllers.push(controllerInfo);
          }
        }
      )
    );

    return controllers;
  }
;

/**
 * openapi格式生成
 * @param params 参数
 * @param ignore 是否忽略字段
 * @param obj 保存到的对象
 * @param config 配置信息
 */
let gen = (params: param[], ignore: boolean = false, obj: any = {}, config: config) => {
  //是否排除字段
  function isExclude(param: param, config: config) {
    if (isEmpty(config.excludeFields)) {
      return false;
    } else {
      if (config.excludeFields.includes(param.name)) {
        return true;
      }
    }
    return false;
  }

  params?.forEach(param => {
    if (ignore && param?.attr?.ignored) {
      return;
    }
    if (param.type === 'array') {
      const arrDatas = {};
      const requireds = param.children?.filter(_ => !_?.attr?.ignored && _?.attr?.require).map(_ => _.name);
      param.children?.forEach(_ => {
        gen([_], ignore, arrDatas, config);
      });
      const isObject = param.children?.length > 1 || param.children[0]?.attr.isObject;
      obj[param.name] = {
        type: 'array',
        title: param.desc,
        items: isObject
          ? {
            type: 'object',
            properties: arrDatas,
            required: requireds,
          }
          : Object.values(arrDatas)[0],
      };
    } else if (param.type === 'object') {
      const objData = {};
      const requires = param.children?.filter(_ => _?.attr?.require).map(_ => _.name);
      gen(param.children ?? [], ignore, objData, config);
      if (!isExclude(param, config)) {
        obj[param.name] = {
          type: 'object',
          title: param.desc,
          properties: objData,
          required: requires,
        };
      }
    } else if (param.type === 'binary') {
      if (!isExclude(param, config)) {
        obj[param.name] = {
          type: 'string',
          format: param.type,
          title: param.desc,
        };
      }
    } else {
      if (!isExclude(param, config)) {
        obj[param.name] = {
          type: param.type,
          title: param.desc,
        };
      }
    }
  });
};

/**
 * schema 格式生成
 * @param obj 参数数组对象
 * @param ignore 是否忽略字段
 * @param config 配置信息
 */
let genSchema = (obj: param[], ignore: boolean = false, config: config) => {
  let schema = {};
  let res = {};
  const required = !isEmpty(obj) ? obj.filter(_ => !_?.attr?.ignored && _?.attr?.require).map(_ => _.name) : [];
  gen(obj, ignore, res, config);
  schema['type'] = 'object';
  schema['properties'] = res;
  schema['required'] = required;

  return schema;
};

function generatorParameters(method: controller['methods'][0]) {
  if (method.requestType === Media.FORM_DATA) {
    return method.params.map(_ => {
      return {
        name: _.name,
        in: "formData",
        title: _.desc,
        required: _.attr?.require ?? false,
        type: _.type === 'binary' ? 'file' : 'string',
      }
    });
  }
  return undefined;
}

function generatorRequestBody(method: controller['methods'][0], config: config) {
  if (method.requestType === Media.FORM_DATA) {
    return null;
  }
  return {
    content: {
      [`${method.requestType.toString()}`]: {
        schema: genSchema(method.params, true, config),
      },
    },
  }
}

function generatorResponse(method: controller['methods'][0], config: config) {
  if (method.responseType === Media.OCTET_STREAM) {
    return {
      '200': {
        description: '文件流下载',
      },
    };
  } else {
    return {
      '200': {
        description: '成功',
        content: {
          [`${method.responseType.toString()}`]: {
            schema: genSchema(method.return, true, config),
          },
        },
      },
    };
  }
}

function generatorFactory(config: config) {
  const controllers = func(config);
  const tags = [
    ...new Set(
      controllers.map(_ => {
        return _.methods.length > 0 ? _.command : '';
      }).filter(_=> _!== '')
    ),
  ];
  const paths = {};
  controllers?.forEach(control => {
    control.methods?.forEach(method => {
      paths[`${control.url.toString()}${method.url.toString()}`] = {
        post: {
          summary: method.command,
          operationId: control.flag+'.'+method.flag,
          deprecated: false,
          tags: [control.command],
          description: '',
          consumes: [method.requestType.toString()],
          parameters: generatorParameters(method),
          requestBody: generatorRequestBody(method, config),
          responses: generatorResponse(method, config),
        },
      };
    });
  });

  //开始解析为openapi 接口
  return {
    openapi: '3.0.0',
    info: {
      title: '项目输出文档',
      description: '',
      version: '1.0.0',
    },
    tags: tags.map(_ => {
      return {name: _};
    }),
    paths: paths,
  };
}

function buildApiFile(config?: config) {
  config = Object.assign(config ?? {}, {
    scanPath: 'src/controller/**/*.ts',
    excludeFields: ['offset', 'pageSize', 'pageSize', 'currentPage'],
  });
  const openapi = generatorFactory(config);
  //将openapi输出到文件
  const dir = config?.output ?? 'apidoc';
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, {recursive: true});
  }
  fs.writeFileSync(`${dir}/openapi.json`, Buffer.from(JSON.stringify(openapi)));
  return openapi;
}

export {buildApiFile, config};
